接續昨天,我想要對這段程式碼做改善
var result = await GetMemberByNameAsync(name)
.TaskBind(member => member
.Map(m => new Attendance{ name = m.Name })
.Map(CreateAttendanceAsync));
在這邊先幫Task擴寫Bind,因為我之後想要用查詢式,所以順手就把Bind改成SelectMany了
public static class TaskExtension
{
public static async Task<RR> SelectMany<T, R, RR>
(this Task<T> source, Func<T, Task<R>> taskSelector, Func<T, R, RR> resultSelector)
{
T t = await source;
R r = await taskSelector(t);
return resultSelector(t, r);
}
}
像在IEnumerable的SelectMany中用了兩次的foreach來把物件拿出盒子,這裡我也是用了兩次的await來達到類似的效果。
在最早之前講到了Option,在這邊我決定把相同的概念搬到Nullable的物件來
public static class NullExtension
{
public static R? Select<T, R>(this T? source, Func<T, R> f)
where T : class
where R : class
=> source is { }
? f(source)
: null;
public static R? SelectMany<T, R>(this T? source, Func<T, R?> f)
where T : class
where R : class
=> source is { }
? f(source)
: null;
public static RR? SelectMany<T, R, RR>
(this T? source, Func<T, R?> nullSelector, Func<T, R, RR> resultSelector)
where T : class
where R : class
where RR : class
=> source is null
? null
: nullSelector(source) is { } r
? resultSelector(source, r)
: null;
}
這邊只針對參考型別做擴充,如果要對實值型別做擴寫的話請使用Nullable<T>
。
接下來就是魔法的時刻,我可以把原來的code變成這樣
var result =
from t1 in GetMemberByNameAsync(name)
// 為了把後面套用CreateAttendanceAsync拆出去,就需要把null判斷用Task包起來
from a1 in Task.FromResult(t1.Select(Attendance.Create))
from a2 in a1.Select(CreateAttendanceAsync)
select a2;
public class Attendance
{
public string Name { get; set; }
// 在這裡直接用工廠方法方便調用
public static Attendance Create(string name) => new Attendance { Name = name };
}
t1是把GetMemberByNameAsync
的結果取出,所以他的型別是一個string?
,我在第二行將Map
(使用select
)映射成Attendance?
,並且放到Task
monad中,也就是說a1的型別是Attendance?
,以此類推,最後result的型別會是Task<string>?
。這邊因為有了SelectMany的多載,使得我能夠將CreateAttendanceAsync
拆出去,而SelectMany接受的方法是由T → C<T>
,也就是說我必須將string?→Attendance?
再用一層Task容器包住。改用查詢運算式來寫的話整個邏輯是不是清楚非常多呢?我當初看到code的時候訝異了非常久,第一個念頭是這樣到底有沒有辦法編譯過,結果經過一番加工後真的可行!
C#的查詢運算式其實保留了非常大的彈性,為任意的monad擴充專屬的Linq查詢方法,就可以套用到查詢運算式的形式,另外我們可以看到SelecMany真的非常重要,回想一下前面介紹過Monad laws中的Left identity,就是今天範例中查詢運算式裡面第二行阿!